Objectives

In this version of MyRent, we introduce a range of UI Widgets to evolve the UX into something more useful. These widgets will be 'active', meaning that the host Activity will be intercepting and responding to events the user may generate when interacting with the application.

Preview

At the end of this topic our goal is to have a screen looking something like this:

We will have added:

  • Section titles (i.e. LOCATION, STATUS).
  • Button indicating the residence registration date.
  • Checkbox: a tick indicating if the residence is rented.

In the previous lab we introduced a listener for the geolocation input (the latitude-longitude string).

In this lab we shall:

  • Add functionality to the Residence model, namely:
    • A Date field to indicate when the residence was registered with the MyRent app.
      • This shall be instantiated in the Residence constructor.
    • A boolean to indicate the rented status of the residence, that is whether or not the residence has a tenant.
    • We shall introduce a method to return a suitably formatted version of the Date object in the form of a string.
      • This shall be used as the button label in the STATUS section.
    • Setters and getters for the rented boolean.

Restructure MyRent

Continue building the MyRent app that you commenced in the previous lab.

Before we start to expand the project, we need to perform some rearranging so it can be extended in an orderly manner.

This is our current application workspace:

Figure 1: Existing structure

.. and this is a version we used in writing this lab:

Figure 2: Structure following refactoring

Here is a detailed description on how this may be achieved:

  • Select gear icon and untick Compact Empty Middle Packages. Observe the change to the package layout.
    Figure 3: Expand empty middle packages

  • Select the myrent package and create a package named activities within org.wit.myrent. Figure 4: Creating new package Figure 5: New package org.wit.myrent.activities

  • Drag and drop MyRentActivity into this new package.

  • Create a package named models within org.wit.myrent.

  • Drag and drop Residence into this new package. Figure 6: Refactored layout

  • Ensure that when the refactoring is complete you tick this menu item again resulting in the required layout as shown in Figure 2 above and again here in Figure 7. Figure 7: Structure following refactoring

Layout

Before proceeding, use the menu refactor command to rename MyRentActivity to ResidenceAcivity.

  • This change is being made to provide naming consistency with future classes that we shall be developing.

The previous iteration has one UI control, an EditText.

This iteration shall introduce:

  • 2 section labels (LOCATION & STATUS).
  • A button that will have a label representing the residence registration date and that will be added at runtime.
  • A checkbox which when ticked indicates that the residence has a tenant.

Button

Select activity_myrent.xml, right click and using context menu commands Refactor | Rename change the name to activity_residence.xml.

Open activity_residence.xml.

Remove background, text and hint colouring (if not already done). This was introduced simply to demonstrate how it might be achieved. The refactored file is as follows:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.wit.myrent.activities.ResidenceAcivity">

    <EditText
        android:id="@+id/geolocation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="16dp"
        android:hint="@string/geolocation_hint"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />

</android.support.constraint.ConstraintLayout>

A number of different approaches are available to change the layout.

  • Here we shall use a both the Design view (graphical) and Text View (XML).

In Design View, drag a Button onto the MyRent canvas. Rezise, position and rename it as shown in Figure 1.

Figure 1: Drag and drop button onto MyRent canvas and manually reformat

Change the left and right margins for the geolocation EditText field to be 16dp also.

You can see in Figure 1, that the text on the Button is Button. We want to replace this with a string resource called "registration_date", whose value is currently the empty string ("").

Figure 2: Removing hardwired strings

Here is the resulting strings.xml file:

<resources>
    <string name="app_name">MyRent</string>
    <string name="hello_world">Hello World!</string>
    <string name="geolocation_hint">52.253456,-7.187162</string>
    <string name="registration_date" />
</resources>

Open activity_residence.xml in Text mode. Your XML should look something like this:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.wit.myrent.activities.ResidenceAcivity">

    <EditText
        android:id="@+id/geolocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginTop="16dp"
        android:hint="@string/geolocation_hint"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginTop="16dp"
        android:text="@string/registration_date"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/geolocation" />

</android.support.constraint.ConstraintLayout>

When run, your activity should look like this: Figure 3: Layout of EditText and Button

We want to change to LinearLayout, so return to acivity_residence.xml and manually refactor your ConstraintLayout to be a LinearLayout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:orientation="vertical">

    <EditText
        android:id="@+id/geolocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/geolocation_hint"/>

    <Button
        android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

Observe that a button id has now been generated in the R.java file:

Figure 4: R File

Next we shall add a section label and divider immediately before the geolocation node.

  • Use the graphical editor and drag a TextView element into position.
  • Then inspect the code in the xml editor.

    • Ensure the width is set to match parent.
    • Change the default android:text to @string/location. Use ALT+Enter to set the string resource value to Location.
    • Style the TextView with a list separator.
  • In res/values/string.xml, the following string resource should have been added:

    <string name="location">Location</string>

Here is the completed xml node for the location label:

    <TextView
        android:id="@+id/locationLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/location"
        style="?android:listSeparatorTextViewStyle"/>

Implement these modifications and inspect the result in the Graphical Layout. You should be presented with that shown in Figure 5:

Figure 5: List separator added

Finally, in this step, add a section label for status.

Here is the xml:

    <TextView
        android:id="@+id/statusLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status"
        style="?android:listSeparatorTextViewStyle"/>

Add the referenced string resource status in res/values/strings.xml:

    <string name="status">Status</string>

The result is shown in Figure 6.

Figure 6: Status section label and separator added

We shall continue with the development of the layout in the following steps.

Here is the refactored activity_residence layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/locationLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/location"
        style="?android:listSeparatorTextViewStyle"/>

    <EditText
        android:id="@+id/geolocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/geolocation_hint"/>

    <TextView
        android:id="@+id/statusLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status"
        style="?android:listSeparatorTextViewStyle"/>

    <Button
        android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

Add Widgets

We shall now complete remaining work on the layout using the Graphical Layout and the Outline panel.

Here is the the layout at this stage of development. Note that we added some comments and requested focus for the geolocation field:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:orientation="vertical">

    <!-- LOCATION SEPARATOR -->
    <TextView
        android:id="@+id/locationLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/location"
        style="?android:listSeparatorTextViewStyle"/>

    <!-- Geolocation (GPS Coords) -->
    <EditText
        android:id="@+id/geolocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/geolocation_hint">
        <requestFocus/>
    </EditText>

    <!-- STATUS SEPARATOR-->
    <TextView
        android:id="@+id/statusLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status"
        style="?android:listSeparatorTextViewStyle"/>

    <Button
        android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

It remains only to add the checkbox.

With the Graphical Layout open, drag a CheckBox from the Widgets pane and drop directly underneath the registration_date button.

Figure 1: Drag and drop CheckBox widget directly underneath registration_date button

Replace android:text attribute with a string, isrented, referenced in strings.xml and change the android:id, all as shown here:

    <CheckBox
        android:id="@+id/isrented"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/isrented"
        android:checked="false"/>

Add a string resource for the checkbox:

  <string name="isrented">Rented?</string>

Because we have moved ResidenceActivity to a new folder, check the manifest file to make sure it was updated (it should have been during the refactoring process):

        <activity android:name=".activities.ResidenceAcivity">
  • The layout so far, is shown in Figure 2:

Figure 2: Layout so far

The hierarchical arrangement of the layout is shown here in Figure 3.

  • Notice that the LinearLayout orientation is vertical thus creating a stack of xml nodes comprising the various UI widgets.

Figure 3: Hierarchical arrangement of UI components

You will notice that the margins are <?xml version="1.0" encoding="utf-8"?>

<!-- LOCATION SEPARATOR -->
<TextView
    android:id="@+id/locationLabel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/location"
    style="?android:listSeparatorTextViewStyle"/>

<!-- Geolocation (GPS Coords) -->
<EditText
    android:id="@+id/geolocation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:hint="@string/geolocation_hint">
    <requestFocus/>
</EditText>

<!-- STATUS SEPARATOR-->
<TextView
    android:id="@+id/statusLabel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/status"
    style="?android:listSeparatorTextViewStyle"/>

<Button
    android:id="@+id/registration_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"/>

<CheckBox
    android:id="@+id/isrented"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/isrented"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:checked="false"/>

slightly differnt in the above screen shot. Move the setting of the margins from the overall LinearLayout and into the EditText, Button and CheckBox widgets only. Note the changes to the output.

Figure 4: Layout completed

And the completed XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- LOCATION SEPARATOR -->
    <TextView
        android:id="@+id/locationLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/location"
        style="?android:listSeparatorTextViewStyle"/>

    <!-- Geolocation (GPS Coords) -->
    <EditText
        android:id="@+id/geolocation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:hint="@string/geolocation_hint">
        <requestFocus/>
    </EditText>

    <!-- STATUS SEPARATOR-->
    <TextView
        android:id="@+id/statusLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status"
        style="?android:listSeparatorTextViewStyle"/>

    <Button
        android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"/>

    <CheckBox
        android:id="@+id/isrented"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/isrented"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:checked="false"/>

</LinearLayout>

Activity & Model Updates

Replace your Residence class with the following:

package org.wit.myrent.models;

import java.util.Date;
import java.util.Random;

public class Residence
{
  public Long id;
  public Long date;

  //a latitude longitude pair
  //example "52.4566,-6.5444"
  private String geolocation;
  public boolean rented;

  public Residence()
  {
    id = unsignedLong();
    date = new Date().getTime();
  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }

  public void setGeolocation(String geolocation)
  {
    this.geolocation = geolocation;
  }

  public String getGeolocation()
  {
    return geolocation;
  }

  public String getDateString() {
    return "Registered:" + dateString();
  }

  private String dateString() {
    String dateFormat = "EEE d MMM yyyy H:mm";
    return android.text.format.DateFormat.format(dateFormat, date).toString();
  }

}

Note that we have made the fields public for convenience. Also, we have introduced a new date and rented fields into the model.

In the ResidenceActivity class, introduce 2 new fields to access the new widgets we have just introduced:

  private CheckBox rented;
  private Button   dateButton;

and on OnCreate, we need to initialize these:

    dateButton  = (Button) findViewById(R.id.registration_date);
    rented      = (CheckBox) findViewById(R.id.isrented);
    rented.setOnCheckedChangeListener(this);

Furthmore, we are going to disable the date button when the activity is created:

    dateButton.setEnabled(false);

We would now like to engage the checkbox rented. First, implement the OnCheckedChangeListener interface:

public class ResidenceActivity extends Activity implements TextWatcher, OnCheckedChangeListener
{

This will require the following import:

import android.widget.CompoundButton.OnCheckedChangeListener;

and this is the implementation:

  @Override
  public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
        Log.i(this.getClass().getSimpleName(), "rented Checked");
        residence.rented = isChecked;
    }

Run the app and test the check button to ensure that the message, rented Checked, is logged when you check and uncheck the box.

This completes the class. Here is the complete code to this stage:

package org.wit.myrent.activities;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;

import org.wit.myrent.R;
import org.wit.myrent.models.Residence;


public class ResidenceActivity extends AppCompatActivity implements TextWatcher, OnCheckedChangeListener{

    private EditText geolocation;
    private Residence residence;
    private CheckBox rented;
    private Button dateButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_residence);

        geolocation = (EditText) findViewById(R.id.geolocation);
        residence   = new Residence();
        geolocation.addTextChangedListener(this);

        dateButton  = (Button)   findViewById(R.id.registration_date);
        dateButton.setEnabled(false);

        rented = (CheckBox) findViewById(R.id.isrented);
        rented.setOnCheckedChangeListener(this);

    }

    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void afterTextChanged(Editable editable) {
        residence.setGeolocation(editable.toString());
    }

    @Override
    public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
        Log.i(this.getClass().getSimpleName(), "rented Checked");
        residence.rented = isChecked;
    }
}

Test

Run the app and check that the layout presented is as expected, something like that shown in Figure 1.

Figure 1: MyRent

Run the app and use the debugger to ensure data is being transmitted to and from the Residence object.

  • Run MyRent app on an emulator.
  • Place a breakpoint at ResidenceActivity.afterTextChanged as shown in Figure 2.
  • Tick the Rented? checkbox.
  • Input a number in the Location input field.
    • The program should halt at the breakpoint.

Examine the state of the variables such as:

  • this
  • editable
  • residence.date
  • residence.geolocation
  • residence.id
  • residence.rented

Press the Run to Cursor toolbar icon to run to completion.

Figure 2: Using debugger to verify data input transmitted to model object

Summary

Here is what we have achieved in this topic:

  • Explored various ways of modifying layout working with the Graphical Layout editor, the XML editor and the Outline view.
  • Added widgets to the layout

    • Section labels and dividers
    • A button to display the date the property was registered
    • A checkbox to indicate whether or not the residence is rented.
  • Added a listener in the controller to detected changes in the UI checkbox state and transmit any state changes to the model Residence object

  • Added a date field to the model and intialized this at the time a residence object created which represents the registration date of the property with the MyRent app.

  • Described how to conduct a simple test using the debugger to verify that the listeners operate correctly and that UI data transmission takes place successfully in both directions between model and UI.

The application at the end of this lab is available for download from GitHub: android-myrent-2017

Select Releases, followed by V1.0.